使用 JS 实现拖放能力所遇到的坑

阅读量:2030 字数:3053 阅读时间:7min

如何实现一个简单的拖放效果

使用 HTML 的 Drag & Drop API 即可快速实现,基本步骤为:

  • 定义可拖拽元素:为允许拖放的 HTML 标签设置 draggable 属性;
  • 定义拖拽数据:定义拖拽动作中所包含的数据项;
  • 定义放置区:为放置区绑定响应事件来响应放置动作。

具体实现步骤,本文不再赘述,可参考 MDN 文档。本文将谈谈,在按照上述步骤实现 D&D (Drag and Drop)组件时所踩到的坑。

拖放冲突

试想一下这个场景:你实现了一个框架,将页面划分为若干模块,每个模块的位置可由用户通过手动拖放来自由调整,实现方法如上所述。但是,某些模块中的业务功能,也有拖放能力,这时外层框架和内层业务功能的拖放可能会出现冲突:外层框架的拖放事件会被内层捕获放置区捕获、内层的拖放事件会被外层框架捕获。而我们期望的是框架的拖放能力仅限于外层,各业务模块内部的拖放能力不超出此模块。因此,需要考虑好拖放隔离

为每个 D&D 组件划分拖放作用域。例如,上述案例中可划分出作用域:框架域、业务功能域 1、业务功能域 2 ......

划分作用域后,需要在拖拽数据项的数据结构里加上此作用域。例如:

<div
    draggable
    className="module draggable"
    onDragStart={(e) => {
        // 添加拖拽数据
        e.dataTransfer.setData("text/plain", JSON.stringify({
            scope: 'framework',  // 表明当前元素仅可在框架作用域进行拖放
            data: { moduleName: 'live-preview' },  // 实际要传输的数据
        }));
        // ......
    }}
/>

为可拖拽(draggable)元素及拖拽数据(transfer data)明确了作用域后,还需要在每个可放置元素(droppable)元素的 onDrop 事件的处理函数内,先判断当前拖拽数据是否是来自合适作用域。示例代码如下:

<div
    className="module droppable"
    onDrop={(e) => {
        // 判断作用域
        let correctZone = true;
        try {
            const transferData = e.dataTransfer.getData("text/plain");
            const { scope } = JSON.parse(transferData);
            
            correctZone = scope !== 'framework'
        } catch {
            correctZone = false;
        }
        if (!correctZone) {
            // 当前拖拽数据不是来自框架,可能来自内部业务功能,
            // 则不响应此放置事件。
            return;
        }
        // 接以下代码

当然,如果找到了正确的放置区(drop zone),应该阻止 onDrop 事件继续冒泡

        // 接以上代码
        else {
            e.stopPropagation();
        }
        // ......
    }}
/>

至此拖拽冲突的问题得以解决。

拖拽数据为 JSON 时需要先序列化

DataTransfer 对象用于保存 D&D 过程中的数据。它可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型。

在使用 DataTransfer.setData() 方法用来设置拖放操作的的数据和类型时,如果数据是 JSON 格式,需要先序列化为字符串。

// Wrong:
event.dataTransfer.setData("application/json", { "name": "Mark Sanders" });

// Correct:
event.dataTransfer.setData("text/plain", JSON.stringify({ "name": "Mark Sanders" }));

仅 DragStart 和 Drop 事件能访问拖拽数据

在 D&D 过程中,你是无法通过除 onDragStartonDrop 以外的其它事件来访问 transfer data 的,这或许是出于安全性考虑。浏览器将 D&D 产生的拖拽数据存放在 data store 中,但它的访问权限受事件类型限制,详情如下:

读/写权限: onDragStart

只读: onDrop

受保护: 其它任何事件

<div
    className="module droppable"
    onDragOver={(e) => {
        // NOT working.
        console.log(e.dataTransfer.getData("text/plain");
    }}
    onDrop={(e) => {
        // Works fine.
        console.log(e.dataTransfer.getData("text/plain");
    }}
/>

可放置区必须设置 onDragOver

我们往往会以为,可放置区只要响应 onDrop 事件即可,事实上,还必须同时设置 onDragOver,并阻止这个事件的其它处理过程。上代码:

<div
    className="module droppable"
    onDragOver={(e) => {
        // 下一行必不可少,否则你会发现 onDrop 事件不会被调用。
        ev.preventDefault();
    }}
    onDrop={(e) => {
        ev.preventDefault();
        // ......
    }}
/>

Tips: 注意每个处理程序调用 preventDefault() 来阻止对这个事件的其它处理过程(如触点事件或指针事件)。

上一篇:Home Sweet Home

本文用于记录发生在我温馨小家中的幸福瞬间。持续更新。 31 Jul 25 Jul 19 Jul 17 Jul

下一篇:谈兴趣

谈兴趣 作为一个从小便痴迷于 coding 并不断尝试借此能力来开发些小玩意的人;作为一个在高考填志愿时认准计算机科学专业的人;作为一个在就业时锚定前端开发工程师这个方向的人,我一直误认为这是「兴趣」所驱动的。 同样,作为一个从小学、初中、高中、大学就极度害怕数学、抵触数学的人,我也误认为这是「不感兴趣」造成的。 兴趣是什么?兴趣是一种与生俱来的标签吗?是上帝在造人时所倾向的属性点配置吗?很

雁过留痕,风过留声

目 录